Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

14.Gün - Swift Optionals ve Nil Coalescing

Bu bölümde Swift’in optional olarak bilinen null reference’lara (değeri var olmayan değişken) yönelik çözümlerini inceleyeceğiz. Özünde bir optional “değişkenimizin herhangi bir değeri yoksa ne olur?” sorusuna cevap vermeye çalışır.

Optional’ler ile Eksik Veri Nasıl İşlenir #

Swift’in null reference’e karşı çözümlerinden biri olan optionals’lar “bu şeyin bir değeri olabilir veya olmayabilir” anlamına gelmektedir.

Şu kodu düşünelim;

let opposites = [
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites["Peach"]

İki tane key değeri olan [String : String] dictionary oluşturuyoruz. Daha sonra opposites Dictionary’de olmayan “Peach” key’ine bağlı değeri okumaya çalıştık. Bu kod çalıştığında peachOpposite ’ın değeri ne olacak?

Swift’in bu soruna yönelik çözümü optionals olarak adlandırılmaktadır, bu da mevcut olabilecek veya olmayabilecek veriler anlamına gelir. Optionals olacak veriler, veri türünden sonra ? işareti ile temsil edilirler. Yani bu durumda; peachOpposite bir String yerine String? olacaktır.

Optionals, içinde bir şey olabilecek veya olmayabilecek bir kutu gibidir. Yani bir String? kutusu içinde bizi bekleyen bir string olabilir ya da hiçbir şey olmayabilir (bu da nil olarak adlandırılan ve “değer yok” anlamına gelen özel bir değer). Her türlü veri optional olabilir; Int, Double, Bool, enum instance’ı, struct instance veya class instance.

Peki burada değişen nedir? Daha önce String vardı şimdi String? var, fark nerede?

Fark şu: Swift kodumuzun tahmin edilebilir olmasını ister, bu da orada olmayan verileri kullanmamıza izin vermeyeceği anlamına gelir. Optionals söz konusu olduğunda, bu optionals’ı kullanmak için onu açmamız gerektiği anlamına gelir.

Swift bize optionals’ı açmak için iki temel yol sunar, ancak en çok göreceğimiz şuna benzer;

if let marioOpposite = opposites["Mario"] {
    print("Mario's opposite is \(marioOpposite)")
}

Bu if let sözdizimi Swift’te çok yaygındır ve bir koşul (if) ile bir sabit(let) oluşturmayı birleştirir. Birlikte üç şey yapar;

  1. Dictionary’den optional değeri okur.
  2. Eğer optional içinde bir string varsa, bu string unwrap edilir. unwrap edilmiş veri marioOpposite sabitine yerleştirilir.
  3. Eğer koşul başaralı olduysa, yani optional unwrap edildiyse koşulun gövdesi çağrılır.

Koşulun gövdesi yalnızca optional içinde bir değer varsa çalıştırılacaktır. Elbette istersek bir else bloğu da ekleyebiliriz. Bu sadece bir if koşuludur.

var username: String? = nil

if let unwrappedName = username {
    print("We got a user: \(unwrappedName)")
} else {
    print("The optional was empty.")
}

Optionals’i biraz Schrödinger’in veri tipi gibi düşünebiliriz. Kutunun içinde bir değer olabilir veya olmayabilir, ancak bunu öğrenmenin tek yolu kontrol etmektir.

Optionals verilerin mevcut olabileceği veya olmayabileceği anlamına gelirken, optionals olmayanların (non-optional), String, integer vb. verilerinin mevcut olması gerekir.

Şöyle düşünelim; elimizde optional olmayan bir Int varsa içinde kesinlike bir sayı var demektir. Buna karşın nil olarak ayarlanmış optional bir Int ’in hiçbir değeri yoktur 0 veya başka bir sayı değil ,hiçbir şeydir.

Array ve Dictionary gibi collection’lar da dahil olmak üzere, gerektiğinde her veri türü optional olabilir. Yine Int sayılardan oluşan bir Array bir veya daha fazla sayı içerebilir veya belki de hiç sayı içermeyebilir, ancak bunların her ikisi de nil olarak ayarlanmış optional Array’den farklıdır.

Açık olmak gerekirse, nil olarak ayarlanmış optional bir Int, 0 değerini tutan optional olmayan bir Int ile aynı değildir. Burada bahsettiğimiz nil ile herhangi bir verinin yokluğundan bahsediyoruz.

Optional olmayan bir Int bekleyen fonksiyona, optional bir Int’i parametre olarak geçmek istediğimizde bu problemi görebiliriz.

func square(number: Int) -> Int {
    number * number
}

var number: Int? = nil
print(square(number: number))

swift optional vs non optional

Swift bu kodu oluşturmayı reddedecektir, çünkü optional bir Int unwrap edilmelidir.

Dolayısıyla, optional’ı unwrap etmeliyiz.

if let unwrappedNumber = number {
    print(square(number: unwrappedNumber))
}

Optional unwrap yapılırken, onları aynı isimde bir sabite (let) açmak çok yaygındır. Swift buna izin vermektedir.

Bu yaklaşımı kullanarak, kodumuzu tekrar yazalım;

if let number = number {
    print(square(number: number))
}

Burada olan şey, geçici olarak aynı isimde, yalnızca koşulun gövdeside kullanılabilen ikinici bir sabit oluşturmamızdır. Buna shadowing denilmektedir ve genellikle unwrap işleminde kullanılır.

Ayrıca yukarıdaki kod bloğunu daha kısa bir şekilde de yazabiliriz;

if let number {
    print(square(number: number))
}

if let number ile if let number = number aynı şeyi yapar, sadece koşulun gövdesi içinde unwrap edilen shadow kopyasını oluşturur.

Swift guard Nedir? Nasıl Kullanılır? #

Optional’leri unwrap etmek için if let ’in nasıl kullanılacağını görmüştük. Hemen hemen aynı şeyi yapan ikinci bir yol daha vardır: guard let

func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}

if let gibi guard let de, optional içinde bir değer olup olmadığını kontrol eder ve varsa değeri alır ve seçtiğimiz sabite yerleştirir.

Fakat işleri biraz ters şekilde yapar.

var myVar: Int? = 3

if let unwrapped = myVar {
    print("myVar içinde değer varsa çalışır")
}

guard let unwrapped = myVar else {
    print("myVar içinde değer yoksa çalışır")
}

if let optional öğe bir değere sahipse parantez içindeki kodu çalıştırır. guard let optional öğe bir değere sahip değilse parantez içindeki kodu çalıştırır. Kodumuzdaki else kullanımı da bu şekilde açıklanmış oluyor: “optional öğeyi unwarp edebilir miyiz kontrol et, ancak açamazsak…..”

guard let kontrol başarısız olursa mevcut fonksiyon, döngü veya koşuldan çıkmak üzere tasarlanmıştır, bu sebeple guard let kullanarak unwrap ettiğimiz tüm değerler kontrolden sonra da kalır.

Buna bazen early return ismi de verilmektedir. fonksiyon başlarken fonksiyonun tüm girdilerinin geçerli olup olmadığını kontrol ederiz ve geçerli olmayanlar varsa biraz kod çalıştırıp hemen çıkarız. Tüm kontrollerimiz geçerse, fonksiyonumuz tam olarak amaçlandığı gibi çalışır.

guard tam olarak bu tarz programlama için tasarlanmıştır ve aslında iki şey yapar;

  1. Bir fonksiyonun girdilerinin geçerli olup olmadığını kontrol etmek için guard kullanırsak, kontrol başarısız olursa Swift her zaman return kullanmamızı isteyecektir.
  2. Kontrol geçerse ve unwarp ettiğimiz optional öğenin içinde bir değer varsa guard kodu bittikten sonra bunu kullanabiliriz.

Az önce yazdığımız printSquare fonksiyonunu incelersek bu iki noktayı görebiliriz.

func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")

        // 1: Fonksiyondan burada çıkmak *zorundayız*
        return
    }

    // 2: `number` hala `guard` dışında kullanılabilir
    print("\(number) x \(number) is \(number * number)")
}

Kısaca; if let ’i optional’ları açmak için kullanın, böylece bu optioanal öğeler ile işlem yapabiliriz. guard let ’i ise optional’ların içinde bir şey olduğundan emin olmak fakat içinde değer yoksa çıkmak için kullanın.

Optional unwrap etmeyenler de dahil olmak üzere herhangi bir koşulla guard kullanabiliriz. guard someArray.isEmpty else {return}

Nil Coalescing Kullanarak Optionals Unwrap #

nil coalescing operator olarak adlandırılan ve bir optional’ı açmamızı, eğer optional boşsa default(varsayılan) değer atamamızı sağlayan bir yöntem daha bulunmaktadır.

let captains = [
    "Enterprise": "Picard",
    "Voyager": "Janeway",
    "Defiant": "Sisko"
]

let new = captains["Serenity"]

Yukarıdaki kod captains dictionary’de var olmayan bir key’i okur. Bu, new sabitinin nil olarak ayarlanan optional String olacağı manasına gelir.

?? şeklinde yazılan nil coalescing operatörü ile herhangi bir optional öğe için aşağıdaki gibi varsayılan bir değer sağlayabiliriz.

let new = captains["Serenity"] ?? "N/A"

Yukarıdaki kod, değeri captains dictionary’den okuyacak ve onu açmaya çalışacaktır. Optional içinde bir değer varsa geri gönderilecek ve new ’de saklanacaktır, ancak değer yoksa bunun yerine “N/A” kullanılacaktır.

Bu durumda, optional ne içerirse içersin (bir değer veya nil) sonuçta new ’in optional değil gerçek bir string olacağı anlamına gelir. Yani new’in değeri ya captains içindeki bir string olacak ya da “N/A” olacaktır.

Elbette dictionary’den değer okurken de varsayılan değer sağlayabiliriz. O zaman nil coalescing ‘in avantajı nedir?

let new = captains["Serenity", default: "N/A"]

Yukarıdaki kod tamamen aynı sonucu üretir. Ancak nil coalescing operator yalnızca dictionary ile değil tüm optional’ler ile çalışabilir.

Örneğin, Array’lerdeki randomElement() methodu Array’den rastgele bir element döndürür, ancak boş bir Array üzerinden de bu methodu çağırma ihtimalimize karşın optional değer döndürür. Bu nedenle bir varsayılan sağlamak için, nil coalescing operator kullanabiliriz.

let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"

Ya da optional bir property’ye sahip bir Struct’ımız vardır ve değeri olmadığı durumlar için mantıklı bir varsayılan değer sağlamak istiyoruzdur;

struct Book {
    let title: String
    let author: String?
}

let book = Book(title: "Beowulf", author: nil)
let author = book.author ?? "Anonymous"
print(author)

Bir string’den bir Int oluşturduğumuzda bile kullanışlıdır, burada aslında optional bir Int? geri alırız çünkü dönüştürme başarısız olmuş olabilir. “Hello” gibi Int’e dönüştürülemeyecek bir string sağlamış olabiliriz.

let input = ""
let number = Int(input) ?? 0
print(number)

Optional Chaining Kullanılarak Birden Fazla Optional Nasıl İşlenir #

Optional chaining, optional’lerin içindeki optional’leri okumak için basitleştirilmiş bir sözdizimidir(syntax).

let names = ["Arya", "Bran", "Robb", "Sansa"]

let chosen = names.randomElement()?.uppercased() ?? "No one"
print("Next in line: \(chosen)")

Optional chaining “optional içinde bir değer varsa onu aç ve sonra….” dememizi sağlar ve daha fazla kod ekleyebiliriz. Bizim kodumuzda ise “eğer Array’den rastgele bir eleman almayı başardıysak, o zaman onu büyük harfle yaz” diyoruz. randomElement() ’in bir optional döndürdüğünü unutmayalım.

Optional chaining’in avantajı, optional boşsa hiçbir şey yapmamasıdır. Sadece daha önce sahip olduğumuz aynı optional’ı hala boş olarak geri gönderecektir. Bu optional chaining’in dönüş değerinin her zaman optional olduğu anlamına gelir, bu nedenle varsayılan bir değer sağlamak için hala nil coalescing operator’e ihtiyacımız vardır.

Optional chaining istediğimiz kadar uzun olabilir ve herhangi bir bölüm nil değerini geri gönderdiğinde kod satırının geri kalanı yok sayılır ve nil değerini geri gönderir.

Optional chaining ’e örnek olması için kitapları yazar adlarına göre alfabetik sıraya yerleştirmek istediğimizi düşünelim. O halde;

  • Bir Book struct’ın optional bir instance’ına sahibiz. (Sıralanacak bir kitabımız olabilir veya olmayabilir)
  • Kitabın bir yazarı olabilir veya anonim olabilir.
  • Author string’i mevcutsa, boş bir string olabilir veya metin içerebilir. Bu nedenle her zaman ilk harfin orada olduğuna güvenemeyiz.
  • İlk harf varsa büyük harf olduğundan emin olalım, böylece bell hooks gibi küçük harfli isimleri olan yazarlar doğru şekilde sıralanır.
struct Book {
    let title: String
    let author: String?
}

var book: Book? = nil
let author = book?.author?.first?.uppercased() ?? "A"
print(author)

Yani “eğer elimizde bir kitap varsa, kitabın bir yazarı varsa ve yazarın bir ilk harfi varsa, o zaman büyük harfle yaz ve geri gönder aksi takidirde “A”’yı geri gönder” demiş oluyoruz.

Optionals ve Function Error #

Hata verebilecek (throw error) bir fonksiyon çağırdığımızda, ya try kullanarak çağırırız ve hataları uygun şekilde ele alırız ya da fonksiyonun başarısız olmayacağından eminsek try! kullanarak kodumuzun çökme riskini alırız.

Bununla birlikte bir alternatif daha vardır. Tek önemsediğimiz fonksiyonun başarılı ya da başarısız olmasıysa, fonksiyonun optional bir değer döndürmesini sağlamak için optional bir try kullanabiliriz. Fonksiyon herhangi bir hata vermeden çalıştıysa, optional geri dönüş değerini içerecektir, ancak herhangi bir hata fırlattıysa nil değeri döndürecektir. Bu, tam olarak hangi hatanın atıldığını bilemeyeceğimiz anlamına gelir ama bu sayede sadece fonksiyonun çalışıp çalışmadığını önemseyebiliriz.

enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -> String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    print("User: \(user)")
}

getUser() fonksiyonu her zaman bir networkFailed hatası fırlatacaktır (test yapabilmek için en iyisi) ancak hangi hatanın fırlatıldığını önemsemiyoruz. Tek önemsediğimiz fonksiyon çağrısının bir user geri gönderip göndermediğidir.

Burada try? yardımcı olur. getUser() fonksiyonunun optional bir string göndermesini sağlar, bu string herhangi bir hata fırlatılırsa nil olacaktır. Tam olarak hangi hatanın olduğunu bilmek istiyorsak, bu yaklaşım yararlı olmayacaktır.

İstersek try? ile nil coalescing ‘i birleştirebiliriz. Bu durumda “bu fonksiyondan geri dönüş değerini almaya çalış, ancak başarısız olursa bunun yerine bu varsayılan değeri kullan” demiş oluruz. Bunun kod hali;

let user = (try? getUser(id: 23)) ?? "Anonymous"
print(user)

try? İfadesi Nerelerde Kullanılır #

  1. try? çağrısı nil döndürürse geçerli fonksiyondan çıkmak için guard let ile birlikte.
  2. Bir fonksiyonu çağırmak fakat hata olması durumunda varsayılan değer sağlamak için nil coalescing ile birlikte.
  3. Dönüş değeri olmayan herhangi bir throw fonksiyonu çağırırken, başarılı olup olmamasını gerçekten önemsemediğimizde. Örneğin, bir log dosyasına yazıyor veya bir sunucuya analiz gönderiyor olabiliriz.

100 Days of SwiftUI Checkpoint - 9 #


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 14 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.